require "Scene.Item.ItemService"

local PriorityQueue = require "loop.collection.PriorityQueue"
local UnorderedArraySet = require "loop.collection.UnorderedArraySet"

--[[
BagService data structure:

while the bag is a container, the container entry is called cell, and the id of
the cell is called slot(identify the postion of cell in bag)

cell = {
	item = object, 	-- item structure
	count = number, -- item count in cell
	state = number, -- cell state, etc: ENABLE...
}

bagService = {
	capacity = number, 	  -- bag capacity
	count = number, 	  -- cell count
	cells = object, 	  -- compressed cell table with tostring(slot) as key
	itemIndexer = object, -- index for quick search with item template id
	freeSlots = object,   -- free cell list for allocation
}

How to make use of the interface of BagService, please refer to BagServiceTest
--]]
BagService = {}

CELL_STATE = {
    ENABLE = 1,  -- Enable state
	DISABLE = 2, -- Disable state
}

local MIN_CAPACITY = 25
local MAX_CAPACITY = 50

local function newCell(item, count)
	return { item = item, count = count, state = CELL_STATE.ENABLE }
end

local function getCell(data)
	local cell = {
		item = ItemService.getItem(data.item),
		count = data.count,
		state = data.state
	}
	return cell
end

local function getData(cell)
	local data = {
		item = ItemService.getData(cell.item),
		count = cell.count,
		state = cell.state
	}
	return data
end

local function addCell(bagService, slot, cell)
	assert(bagService.cells[slot] == nil, "Add existed cell")

	local templateId = ItemService.getTemplateId(cell.item)

	-- allocate bag slot
	bagService.cells[slot] = cell
	bagService.count = bagService.count + 1

	PriorityQueue.remove(bagService.freeSlots, slot)

	-- make item index
	if not bagService.itemIndexer[templateId] then
		bagService.itemIndexer[templateId] = {}
	end
	PriorityQueue.enqueue(bagService.itemIndexer[templateId], slot, tonumber(slot))
end

local function deleteCell(bagService, slot)
	assert(bagService.cells[slot] ~= nil, "Delete nonexist cell")

	local cell = bagService.cells[slot]
	local templateId = ItemService.getTemplateId(cell.item)

	-- free bag slot
	bagService.cells[slot] = nil
	bagService.count = bagService.count - 1

	PriorityQueue.enqueue(bagService.freeSlots, slot, tonumber(slot))

	-- make item index
	PriorityQueue.remove(bagService.itemIndexer[templateId], slot)
end

local function canAddItem(bagService, item, count, reservedSlots)
	if count <= 0 then
		Log.error("Add invalid count " .. tostring(count) .. " for item " .. item.id)
		return false
	end

	local templateId = ItemService.getTemplateId(item)
	local allocated = bagService.itemIndexer[templateId]
	local freeSlots = bagService.freeSlots

	if not allocated then
		allocated = {}
	end

	-- accmulate item to exist cells
	for slot in PriorityQueue.sequence(allocated) do
		local cell = bagService.cells[slot]

		if cell.state == CELL_STATE.ENABLE then
			local reserved = reservedSlots[slot]
			if reserved then
				count = count + reserved.count
			end

			local available = ItemService.canAccmulate(item, count, cell.item, cell.count)
			if available > 0 then
				Log.trace("Reserve slot " .. slot .. " for item " .. item.id .. " count " .. tonumber(available))

				if reserved then
					reserved.count = reserved.count + available
				else
					reservedSlots[slot] = { item = item, count = available }
				end

				count = count - available
				if count == 0 then
					return true
				end
			end
		end
	end

	-- allocate new cells from item
	for slot in PriorityQueue.sequence(freeSlots) do
		local available = 0
		local reserved = reservedSlots[slot]

		if reserved then
			available = ItemService.canAccmulate(item, count, reserved.item, reserved.count)
		else
			available = ItemService.canAdd(item, count)
		end

		if available > 0 then
			Log.trace("Reserve slot " .. slot .. " for item " .. item.id .. " count " .. tonumber(available))

			if not reserved then
				reservedSlots[slot] = { item = item, count = available }
			else
				reserved.count = reserved.count + available
			end

			count = count - available
			if count == 0 then
				return true
			end
		end
	end

	Log.trace("Can't find free slot for item " .. item.id .. " count " .. tostring(count))
	return false
end

local function canAddItems(bagService, itemTable, reservedSlots)
	local added = 0
	for item, count in pairs(itemTable) do
		if not canAddItem(bagService, item, count, reservedSlots) then
			return false
		end
		added = added + 1
	end

	if added == 0 then
		Log.error("Add empty table")
		return false
	end
	return true
end

local function canDeleteCell(bagService, slot, count, deletedSlots)
	slot = tostring(slot)

	if count <= 0 then
		Log.error("Delete invalid count " .. tostring(count))
		return false
	end

	if deletedSlots[slot] then
		count = count + deletedSlots[slot]
	end
	local cell = bagService.cells[slot]

	if not cell then
		Log.error("Delete nonexist cell at slot " .. slot)
		return false
	elseif cell.state ~= CELL_STATE.ENABLE then
		Log.error("Delete abnormal cell at slot " .. slot)
		return false
	elseif count > cell.count then
		return false
	end

	deletedSlots[slot] = count
	return true
end

local function canDeleteItem(bagService, templateId, count, deletedSlots)
	templateId = tostring(templateId)

	if count <= 0 then
		Log.error("Delete invalid count " .. tostring(count))
		return false
	end
	local allocated = bagService.itemIndexer[templateId]

	if not allocated or PriorityQueue.empty(allocated) then
		return false
	end

	for slot in PriorityQueue.sequence(allocated) do
		local cell = bagService.cells[slot]
		local deleteCount = math.min(cell.count, count)

		if canDeleteCell(bagService, slot, deleteCount, deletedSlots) then
			count = count - deleteCount
			if count == 0 then
				return true
			end
		end
	end
	return false
end

local function canDeleteItems(bagService, templateTable, deletedSlots)
	local deleted = 0
	for templateId, count in pairs(templateTable) do
		if not canDeleteItem(bagService, templateId, count, deletedSlots) then
			return false
		end
		deleted = deleted + 1
	end

	if deleted == 0 then
		Log.error("Delete empty table")
		return false
	end
	return true
end

local function addItems(bagService, reservedSlots)
	for slot, element in pairs(reservedSlots) do
		local cell = bagService.cells[slot]
		if cell then
			cell.count = cell.count + element.count
		else
			cell = newCell(element.item, element.count)
			addCell(bagService, slot, cell)
		end
	end
end

local function deleteItems(bagService, deletedSlots)
	for slot, count in pairs(deletedSlots) do
		local cell = bagService.cells[slot]
		cell.count = cell.count - count

		if cell.count == 0 then
			deleteCell(bagService, slot)
		end
	end
end

--[[
initialzie bag service for player, convert bag data to service data structure(
which is more effective and convenient to use but could not be serialized and
used for store)

@player
--]]
BagService.initialize = function(player)
	player.bagService = {
		capacity = MIN_CAPACITY,
		count = 0,
		cells = {},
		itemIndexer = {},
		freeSlots = {},
	}

	local bagData = player.data.b_bag
	local bagService = player.bagService

	if bagData then
		bagService.capacity = bagData.capacity
	end

	for slot = 1, bagService.capacity do
		PriorityQueue.enqueue(bagService.freeSlots, tostring(slot), slot)
	end

	if not bagData or not bagData.cells then
		return -- return immediately if it's a new character or if cells is empty
	end

	for slot, cellData in pairs(bagData.cells) do
		local cell = getCell(cellData)
		addCell(bagService, slot, cell)
	end
end

--[[
flush bag service data for player, convert service data to store data structure

@player
--]]
BagService.flush = function(player)
	local bagService = player.bagService

	local bagData = {
		capacity = bagService.capacity,
		cells = {}
	}

	for slot, cell in pairs(bagService.cells) do
		bagData.cells[slot] = getData(cell)
	end
	player.data.b_bag = bagData
end

BagService.getCapacity = function(player)
	return player.bagService.capacity
end

BagService.getCellCount = function(player)
	return player.bagService.count
end

BagService.clear = function(player)
	local bagService = player.bagService

	bagService.count = 0
	bagService.cells = {}
	bagService.itemIndexer = {}
	bagService.freeSlots = {}

	-- rebuild free slots
	for slot = 1, bagService.capacity do
		PriorityQueue.enqueue(bagService.freeSlots, tostring(slot), slot)
	end

	Client.send(player, "clearBagCells")
end

BagService.extend = function(player, delta)
	if delta <= 0 then
		Log.error("Extend invalid delta " .. tostring(delta))
		return false
	end

	local bagService = player.bagService
	local extendBegin = bagService.capacity + 1
	local extendEnd = bagService.capacity + delta

	if extendEnd > MAX_CAPACITY then
		return false -- TODO: notice client extend exceed limitation
	end

	bagService.capacity = extendEnd
	for slot = extendBegin, extendEnd do
		PriorityQueue.enqueue(bagService.freeSlots, tostring(slot), slot)
	end

	Client.send(player, "updateBagCapacity", bagService.capacity)
	return true
end

BagService.getCell = function(player, slot)
	slot = tostring(slot)

	local bagService = player.bagService
	local cell = bagService.cells[slot]

	if not cell then
		Log.error("Get nonexist cell at slot " .. slot)
		return nil
	elseif cell.state ~= CELL_STATE.ENABLE then
		Log.error("Get abnormal cell at slot " .. slot)
		return nil
	end
	return cell
end

BagService.getState = function(player, slot)
	slot = tostring(slot)

	local bagService = player.bagService
	local cell = bagService.cells[slot]

	-- as could not get a cell when it's not ENABLE,
	-- so for nonexist cell, return DISABLE state
	if cell then
		return cell.state
	else
		return CELL_STATE.DISABLE
	end
end

BagService.setState = function(player, slot, state)
	slot = tostring(slot)

	local bagService = player.bagService
	local cell = bagService.cells[slot]

	if not cell then
		Log.error("Set state for nonexist cell at slot " .. slot)
		return false
	end
	cell.state = state

	Client.send(player, "updateBagState", slot, state)
	return true
end

BagService.bindItem = function(player, slot)
	slot = tostring(slot)

	-- only available cell could be binded
	local cell = BagService.getCell(player, slot)
	if cell then
		if ItemService.bind(cell.item) then
			Client.send(player, "updateBagItem", slot, cell.item)
			return true
		end
		Log.error("Bind item twice at slot " .. slot)
	end
	return false
end

BagService.refreshItem = function(player, slot)
	slot = tostring(slot)

	-- only available cell could be refreshed
	local cell = BagService.getCell(player, slot)
	if cell then
		Client.send(player, "updateBagItem", slot, cell.item)
	end
end

BagService.getItemCount = function(player, templateId)
	templateId = tostring(templateId)

	local bagService = player.bagService
	local allocated = bagService.itemIndexer[templateId]

	if not allocated or PriorityQueue.empty(allocated) then
		return 0
	end

	local total = 0
	for slot in PriorityQueue.sequence(allocated) do
		local cell = bagService.cells[slot]
		total = total + cell.count
	end
	return total
end

BagService.canAddItem = function(player, item, count, reservedSlots)
	local bagService = player.bagService
	return canAddItem(bagService, item, count, reservedSlots)
end

BagService.canAddItems = function(player, itemTable, reservedSlots)
	local bagService = player.bagService
	return canAddItems(bagService, itemTable, reservedSlots)
end

BagService.canDeleteCell = function(player, slot, count, deletedSlots)
	local bagService = player.bagService
	return canDeleteCell(bagService, slot, count, deletedSlots)
end

BagService.canDeleteItem = function(player, templateId, count, deletedSlots)
	local bagService = player.bagService
	return canDeleteItem(bagService, templateId, count, deletedSlots)
end

BagService.canDeleteItems = function(player, templateTable, deletedSlots)
	local bagService = player.bagService
	return canDeleteItems(bagService, templateTable, deletedSlots)
end

BagService.addItems = function(player, reservedSlots)
	local bagService = player.bagService
	addItems(bagService, reservedSlots)
end

BagService.deleteItems = function(player, deletedSlots)
	local bagService = player.bagService
	deleteItems(bagService, deletedSlots)
end

BagService.sort = function(player)
	local bagService = player.bagService

	-- create a new bag service for sort
	local newService = {
		capacity = bagService.capacity,
		count = 0,
		cells = {},
		itemIndexer = {},
		freeSlots = {},
	}

	for slot = 1, newService.capacity do
		PriorityQueue.enqueue(newService.freeSlots, tostring(slot), slot)
	end

	-- get items from original, abnormal cell(LOCKED) should be kept the same
	local itemTable = {}

	for slot, cell in pairs(bagService.cells) do
		if cell.state == CELL_STATE.ENABLE then
			itemTable[cell.item] = cell.count
		else
			addCell(newService, slot, cell)
		end
	end

	local reservedSlots = {}
	canAddItems(newService, itemTable, reservedSlots)
	addItems(newService, reservedSlots)

	player.bagService = newService
	BagService.flush(player)
	Client.send(player, "updateBagData", player.data.b_bag)
end

BagService.divideCell = function(player, slot, count)
	slot = tostring(slot)

	if count <= 0 then
		Log.error("Divide invalid count " .. tostring(count))
		return false
	end

	local cell = BagService.getCell(player, slot)

	if not cell then
		return false
	elseif count >= cell.count then
		Log.error("Divide exceeded count at slot " .. slot)
		return false
	end

	local bagService = player.bagService

	if PriorityQueue.empty(bagService.freeSlots) then
		return false
	end

	local updatedCells = {}
	local templateId = ItemService.getTemplateId(cell.item)
	local item = ItemService.cloneItem(cell.item)

	-- update original cell
	cell.count = cell.count - count
	updatedCells[slot] = getData(cell)

	-- create new cell for division
	slot = PriorityQueue.dequeue(bagService.freeSlots)
	cell = newCell(item, count)

	bagService.count = bagService.count + 1
	bagService.cells[slot] = cell
	updatedCells[slot] = getData(cell)

	-- make item index
	PriorityQueue.enqueue(bagService.itemIndexer[templateId], slot, tonumber(slot))

	Client.send(player, "updateBagCells", updatedCells)
	return true
end

BagService.moveCell = function(player, srcSlot, dstSlot)
	srcSlot = tostring(srcSlot)
	dstSlot = tostring(dstSlot)

	local srcCell = BagService.getCell(player, srcSlot)
	if not srcCell then
		return false
	end

	local bagService = player.bagService
	local dstCell = bagService.cells[dstSlot]

	if dstCell then
		local available = ItemService.canAccmulate(srcCell.item, srcCell.count, dstCell.item, dstCell.count)
		if available > 0 then
			dstCell.count = dstCell.count + available
			srcCell.count = srcCell.count - available

			if srcCell.count == 0 then
				deleteCell(bagService, srcSlot)
			end
		else
			deleteCell(bagService, srcSlot)
			deleteCell(bagService, dstSlot)

			addCell(bagService, dstSlot, srcCell)
			addCell(bagService, srcSlot, dstCell)
		end
	else
		deleteCell(bagService, srcSlot)

		addCell(bagService, dstSlot, srcCell)
	end
	return true
end

BagService.useCell = function(player, slot, count, ...)
	local deletedSlots = {}
	if not BagService.canDeleteCell(player, slot, count, deletedSlots) then
		return false
	end

	local cell = BagService.getCell(player, slot)
	if not ItemService.use(player, cell.item, count, ...) then
		return false
	end
	BagService.deleteItems(player, deleteSlots)
	return true
end

BagService.useItem = function(player, templateId, count, ...)
	local deletedSlots = {}
	if not BagService.canDeleteItem(player, templateId, count, deletedSlots) then
		return false
	end

	for slot, count in pairs(deletedSlots) do
		if not BagService.useCell(player, slot, count, ...) then
			return false
		end
	end
	return true
end